package com.dh.spring.client.file.common;

import com.dh.spring.client.bootstrap.FileCoreServiceHandler;
import com.dh.spring.client.file.download.SocketChannelDownloadHandler;
import com.dh.spring.client.file.upload.SocketChannelUploadHandler;
import com.dh.spring.common.BasicConstant;
import com.dh.spring.util.LocalTime;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.time.LocalDateTime;
import java.util.concurrent.Callable;

/**
 * 文件公共消息处理类
 *
 * */

@Slf4j
@SuppressWarnings("all")
public class CommonReadEventHandler implements Callable<ReadEvent> {
    private final static Logger logger = LoggerFactory.getLogger(CommonReadEventHandler.class);
    private SelectionKey selectionKey;
    private SocketChannel socketChannel;
    private ByteBuffer byteBuffer = ByteBuffer.allocate(5 * 1024*1024);// TODO SocketChannel实时读写缓冲区
    private ByteBuffer cacheBuffer = ByteBuffer.allocate(1024);// TODO 缓存缓冲区(用于缓存一次就绪事件读取到的数据不符合整包数据，需要进行缓存)
    private volatile boolean cache = false;
    private int readEndEOF = -1; //TODO 流读取文件末尾标志
    public String fileName = "", fileExec = "", result = "";
    private long fileSize = 0L; //TODO 文件大小
    private int fileNameLength = 0, writeFileLength = 0;
    private ReadEvent readEvent = new ReadEvent();

    /**
     * TODO 由于TCP协议传输数据的不确定性，无法按照可控的方式完整读取每份数据，此时设置isReadPackageHead判断是否读取出的数据为每份完整数据,即拆包和粘包问题
     * */
    private int isReadFileInfoLength = -1; //TODO 判断是否能够直接读完文件基本信息数据
    private int dataPackageForFileNameLength = 4;//TODO 文件名称长度数据
    private byte[] dataPackageForFileNameLengthBytes = new byte[dataPackageForFileNameLength];
    private byte[] dataPackageForFileNameReturnMessageBytes = null;
    private volatile ByteBuffer buffer = ByteBuffer.allocate(8);

    public CommonReadEventHandler(SelectionKey selectionKey) {
        this.selectionKey = selectionKey;
        this.socketChannel = (SocketChannel) this.selectionKey.channel();
    }

    public ReadEvent getReadEvent() {
        return readEvent;
    }

    @Override
    public ReadEvent call() {
        try {
            while (true) {
                //TODO 设置缓存缓冲区
                this.setCacheBuffer();

                /**
                 * TODO 2、如果缓存缓冲区CacheBuffer有数据了，会在SocketChannel读取到ByteBuffer前，先将缓存缓冲区数据读入ByteBuffer，然后再从SocketChannel中读取
                 * TODO 注意此处读取为追加读取,从SocketChannel中读取数据到byteBuffer中不会覆盖缓存缓冲区提前读取进ByteBuffer中的数据
                 * 关于SocketChannel.read(byteBuffer)返回结果的情况分析:
                 *  = 0: 通道内没有数据可以读取，如果对端SocketChannel没有关闭输出流且没有关闭SocketChannel，则SocketChannel将死循环返回0
                 *      此时可通过本端SocketChannel进行处理，也可以通过对端SocketChannel处理，如果发送完数据，对端应该尝试关闭输出流和关闭SocketChannel
                 *
                 *       (1)、如果对端关闭输出流(shutdownoutputstream)，则SocketChannel.read()会返回-1，能够跳出该SocketChannel循环，但是多路复用器
                 *          的select()方法将无法退出，即selector.select()方法将永远返回0，等待就绪事件,进入select()方法调用的死循环
                 *
                 *       (2)、如果对端关闭连接(调用SocketChannel.close()方法),则SocketChannel.read()会返回-1，能够跳出该SocketChannel循环，但是多路复用器的select()方法将
                 *          永远返回1，即有就绪事件，且就绪事件是上次就绪事件的再次选中，尝试从该就绪事件继续读取时，socketChannel.read()将一直返回-1，用于
                 *          没有数据可读，读取的都是数据结尾-1的数据，这将会导致select()方法和socketChannel.read()两个方法都在无限期死循环阻塞
                 * */
                while ((this.readEndEOF = this.socketChannel.read(this.byteBuffer)) > 0) {
                    this.byteBuffer.flip(); //TODO 切换读模式

                    if(this.byteBuffer.hasRemaining()) {
                        int availableBytes = this.byteBuffer.remaining();
                        if(this.isReadFileInfoLength == -1) {
                            if(availableBytes > this.dataPackageForFileNameLength) {
                                this.dataPackageForFileNameLengthBytes = new byte[this.dataPackageForFileNameLength];
                                this.byteBuffer.get(this.dataPackageForFileNameLengthBytes);//TODO 读取文件名称长度数据
                                this.dataPackageForFileNameReturnMessageBytes = new byte[this.byteArrayToInt(this.dataPackageForFileNameLengthBytes)];
                                this.isReadFileInfoLength = this.dataPackageForFileNameLength;

                                //TODO 开始读取文件名称和文件命令数据
                                if(0 < this.dataPackageForFileNameLength) {
                                    //TODO 如果此if成立，表明剩余的字节数大于文件名称长度字节大小，文件大小以及文件字节数组大小，即可尝试读取文件名字节流数据
                                    if((availableBytes - this.dataPackageForFileNameLength) >= this.dataPackageForFileNameReturnMessageBytes.length) {
                                        this.readReturnFileMessage(availableBytes - this.dataPackageForFileNameLength);
                                    } else { //TODO 剩余字节长度不够文件名称数据长度，可能存在丢包，没能一次发送完整则临时记录能够可读的部分文件名称字节数据
                                        this.cacheBuffer.clear();
                                        byte[] tmpBytes = new byte[availableBytes - this.dataPackageForFileNameLength];
                                        this.byteBuffer.get(tmpBytes, 0, (availableBytes - tmpBytes.length));
                                        this.cacheBuffer.put(tmpBytes);
                                        this.cache = true;
                                        break;
                                    }
                                } else {
                                    throw new RuntimeException("文件名称长度为空，读取失败");
                                }
                            } else {
                                this.canNotReadPackageHeadForOneTime(availableBytes);
                                break;
                            }
                        } else { //TODO 成功读取文件包头开始读取文件名称
                            if((availableBytes - this.dataPackageForFileNameLength) >= this.dataPackageForFileNameReturnMessageBytes.length) {
                               this.readReturnFileMessage(availableBytes - this.dataPackageForFileNameLength);
                            } else {
                                byte[] cacheBytes = new byte[availableBytes];
                                this.byteBuffer.get(cacheBytes, 0, cacheBytes.length);
                                this.byteBuffer.clear();
                                this.cacheBuffer.put(cacheBytes);
                                this.cache = true;
                                break;
                            }
                        }
                    }
                }

                if(this.readEndEOF == 0 && !"".equals(this.readEvent.getResult()) && this.readEvent.getResult().equals("[OK]")/* && ((long) writeFileLength) == filelength */) {
                    logger.info("[ " + this.fileName + " ] 文件反馈信息处理成功......");
                    break;
                }

                this.byteBuffer.clear();

            }
        } catch (Exception e) {
            FileCoreServiceHandler.closeSocketChannel(this.selectionKey, null, "CommonReadEventHandler","校验客户端文件异常, error = " + e.getMessage(), ((SendEntity) this.selectionKey.attachment()).getExec().endsWith(BasicConstant.UPLOAD)?BasicConstant.UPLOAD:BasicConstant.DOWNLOAD, this.fileName, 0);
        }

        return this.readEvent;
    }

    //TODO 1、设置缓存缓冲区(将缓存缓冲区的数据设置到byteBuffer中，用于后续SocketChannel.read(byteBuffer)方调用时追加写)
    private void setCacheBuffer() {
        if(this.cache) {
            this.cacheBuffer.flip(); //TODO 切换为读模式
            if (this.cacheBuffer.hasRemaining()) { //TODO 缓存有可用数据，将缓存缓冲区数据追加到byteBuffer中

                //TODO 1、先从缓存缓冲区读出数据到tmpBytes
                byte[] tmpBytes = new byte[this.cacheBuffer.remaining()];
                this.cacheBuffer.get(tmpBytes, 0, tmpBytes.length);

                //TODO 2、再将数据读入byteBuffer后，此时再从socketChannel读取的新数据是追加到byteBuffer中的，保证了假设拆包存在时，将读取的部分数据先存入缓存缓冲区，
                //TODO 下次读取后一部分数据时先加载前一次读取的半包数据，再此基础上再追加读取
                this.byteBuffer.put(tmpBytes);
                this.cacheBuffer.compact(); //TODO 切换为写模式
                this.cache = false;
            }
        }
    }

    //TODO 2、未能成功一次性读取包头数据，即走缓冲缓存区存储
    private void canNotReadPackageHeadForOneTime(int availableBytes) {
        this.cacheBuffer.flip();//TODO 切换为读取模式
        //TODO 判断读进cacheBuffer中的数据量是否超过CacheBuffer缓冲区总容量，未超过则可以追加读(如果拆包严重，则会出现需要多次进行不完整数据的缓存)
        if(this.cacheBuffer.remaining() < this.cacheBuffer.capacity()) {
            //TODO 切换为写模式,这里是针对CacheBuffer上次写完后的追加写,不管每次读取的可用字节数为多少都先追加
            this.cacheBuffer.compact();

            //TODO 设置按照本次byteBuffer中读取到的数据大小建立临时缓冲区
            byte[] cacheBytes = new byte[availableBytes];
            this.byteBuffer.get(cacheBytes, 0, cacheBytes.length); //TODO 先将SocketChannel中读进byteBuffer缓存中的数据读取到cacheBytes中
            this.byteBuffer.clear(); //TODO 读取完后清空byteBuffer
            this.cacheBuffer.put(cacheBytes); //TODO 再将cacheBytes中的数据读入缓存缓冲区
            this.cache = true;
            this.cacheBuffer.flip(); //TODO 切换读取模式

            //TODO 如果缓存缓冲区中的字节数经过不断的缓存后的数据大小 >= 文件名称长度大小(4字节int)以及文件流长度(8字节long)大小以及文件名称字节数组大小，即可以处理文件信息基础数据(如果大于可能含有文件流数据)
            if(this.cacheBuffer.remaining() >= (this.dataPackageForFileNameLength)) {
                this.dataPackageForFileNameLengthBytes = new byte[this.dataPackageForFileNameLength];
                this.byteBuffer.get(this.dataPackageForFileNameLengthBytes);//TODO 读取文件名称长度数据
                this.dataPackageForFileNameReturnMessageBytes = new byte[this.byteArrayToInt(this.dataPackageForFileNameLengthBytes)];
                this.isReadFileInfoLength = this.dataPackageForFileNameLength;

                //TODO 如果此时缓存缓冲区的数据(可能一次就刚好读取相同或是多次多取达到了相同)刚好等于文件基础信息长度数据，则重置是否使用缓存缓冲区标记，以及清空缓存缓冲区
                if(this.cacheBuffer.remaining() == this.dataPackageForFileNameLength) {
                    this.cache = false;
                    this.cacheBuffer.clear();
                } else {//TODO 如果不等于，那可能存在文件基础数据没有读取完或是文件基础数据读取完成并伴随有文件内容流数据
                    //TODO 切换为写模式，并将已经处理完的包头数据清除，请多余出来的数据移动到CacheBuffer最前头，并将position重置到已存入字节的下一个位置
                    this.cacheBuffer.compact();
                }
            }
        } else {
            throw new RuntimeException();
        }
    }

    //TODO 3、读取文件名称和命令数据 dataPackage = availableBytes - this.dataPackageFileNameLengthFileSizeFileNameBytesFileExecBytesLength(已经除去文件名称和文件流大小后的可用字节)
    private void readReturnFileMessage(int availableBytes) throws IOException {
        this.byteBuffer.get(this.dataPackageForFileNameReturnMessageBytes, 0, this.dataPackageForFileNameReturnMessageBytes.length);
        String returnMessage = new String(this.dataPackageForFileNameReturnMessageBytes, 0, this.dataPackageForFileNameReturnMessageBytes.length);
        this.fileName = returnMessage;

        this.readEvent.setFileExec(((SendEntity) this.selectionKey.attachment()).getExec());
        this.readEvent.setFileName(this.fileName);
        if(returnMessage.endsWith("[FAIL]")) {
            this.readEvent.setResult("[FAIL]");
            log.info("[" + LocalTime.formatDate(LocalDateTime.now()) + "] CommonReadEventHandler | --> 文件不存在，操作失败: file = {fileName = {}}", this.fileName);
        } else {
            if(((SendEntity) this.selectionKey.attachment()).getExec().equals(BasicConstant.UPLOAD)) {
                SocketChannelUploadHandler socketChannelUploadHandler = new SocketChannelUploadHandler(this.selectionKey, ((SendEntity) this.selectionKey.attachment()).getFileName());
                FileCoreServiceHandler.specificChannelRunnableMaps.put(((SendEntity) this.selectionKey.attachment()).getChannelName(), socketChannelUploadHandler);
            }

            if(((SendEntity) this.selectionKey.attachment()).getExec().equals(BasicConstant.DOWNLOAD)) {
                SocketChannelDownloadHandler socketChannelDownloadHandler = new SocketChannelDownloadHandler(this.selectionKey, ((SendEntity) this.selectionKey.attachment()).getFileName());
                FileCoreServiceHandler.specificChannelRunnableMaps.put(((SendEntity) this.selectionKey.attachment()).getChannelName(), socketChannelDownloadHandler);
            }

            this.readEvent.setResult("[OK]");
        }

    }

    //TODO int 转 byte[]
    private byte[] intToBytes(int value) {
        byte[] result = new byte[4];
        // 由高位到低位
        result[0] = (byte) ((value >> 24) & 0xFF);
        result[1] = (byte) ((value >> 16) & 0xFF);
        result[2] = (byte) ((value >> 8) & 0xFF);
        result[3] = (byte) (value & 0xFF);
        return result;
    }

    //TODO byte[] 转 int
    private int byteArrayToInt(byte[] bytes) {
        int value = 0;
        // 由高位到低位
        for (int i = 0; i < 4; i++) {
            int shift = (4 - 1 - i) * 8;
            value += (bytes[i] & 0x000000FF) << shift;// 往高位游
        }
        return value;
    }

    //TODO long 转 byte[]
    private byte[] longToBytes(long x) {
        buffer.clear();
        buffer.putLong(0, x);
        return buffer.array();
    }

    //TODO byte[] 转 long
    private long bytesToLong(byte[] bytes) {
        this.buffer.put(bytes, 0, bytes.length);
        buffer.flip();//need flip
        long result = buffer.getLong();
        buffer.clear();
        return result;
    }
}
